package com.google.gwt.gfx.client;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.DoubleClickEvent;
import com.google.gwt.event.dom.client.DoubleClickHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.HasAllFocusHandlers;
import com.google.gwt.event.dom.client.HasAllKeyHandlers;
import com.google.gwt.event.dom.client.HasAllMouseHandlers;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.dom.client.HasDoubleClickHandlers;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseOverEvent;
import com.google.gwt.event.dom.client.MouseOverHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.dom.client.MouseWheelEvent;
import com.google.gwt.event.dom.client.MouseWheelHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.gfx.client.graphics.Graphic;
import com.google.gwt.gfx.client.graphics.GraphicPanel;
import com.google.gwt.gfx.client.layers.Layer;
import com.google.gwt.gfx.client.overlays.Overlay;
import com.google.gwt.gfx.client.services.RefreshService;
import com.google.gwt.gfx.client.services.Refreshable;
import com.google.gwt.gfx.client.shared.MutableViewport;
import com.google.gwt.gfx.client.shared.Viewport;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Focusable;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.impl.FocusImpl;

/**
 * A type of {@link Panel} for rendering the {@link #layers()}.
 */
public class GFXPanel<V extends Viewport, M extends MutableViewport, S extends RefreshService<V>>
		extends AbsolutePanel implements Focusable, HasAllMouseHandlers,
		HasAllKeyHandlers, HasAllFocusHandlers, HasClickHandlers,
		HasDoubleClickHandlers {

	public static interface Command<B extends GFXPanel<?, ?, ?>> {

		/**
		 * @param buddy
		 *            binded {@link GFXPanel} wraps on.
		 */
		void bind(B buddy);

		/**
		 * @return binded {@link GFXPanel} wraps on.
		 */
		B buddy();

	}

	public static interface Tool<B extends GFXPanel<?, ?, ?>> extends
			Command<B> {

		/**
		 * Disables this tool.
		 */
		void disable();

		/**
		 * Enables this tool.
		 */
		void enable();

	}

	private static FocusImpl focusImpl = FocusImpl.getFocusImplForPanel();

	private GraphicPanel canvasRef = new GraphicPanel();

	private AbsolutePanel contentRef = new AbsolutePanel();

	private AbsolutePanel floatingContentRef = new AbsolutePanel();

	private Collection<Graphic> graphicsRef = new Collection<Graphic>() {

		private List<Graphic> list = new ArrayList<Graphic>();

		@Override
		public boolean add(Graphic g) {
			if (g == null) {
				return false;
			}

			if (!(g instanceof Refreshable<?>)) {
				// TODO check bounds
				canvas().add(g);
				return true;
			}

			Refreshable<?> refreshable = (Refreshable<?>) g;
			if (refreshable.service() != null
					&& refreshable.service().isBindableWith(viewport())
					&& list.add(g)) {
				refreshable.service().tryBind(viewport());
				if (!refreshable.service().isEnabled()) {
					g.style().setVisible(false);
				}
				canvas().add(g);
				g.style().setVisible(true);
				refreshable.refresh();
				return true;
			}

			return false;
		}

		@Override
		public boolean addAll(Collection<? extends Graphic> c) {
			boolean dirty = false;
			for (Graphic g : c) {
				dirty = dirty || add(g);
			}
			return dirty;
		}

		@Override
		public void clear() {
			for (Graphic g : list) {
				if (g instanceof Refreshable<?>) {
					((Refreshable<?>) g).bind(null);
				}
				canvasRef.remove(g);
			}
			list.clear();
		}

		@Override
		public boolean contains(Object o) {
			return list.contains(o);
		}

		@Override
		public boolean containsAll(Collection<?> c) {
			return list.containsAll(c);
		}

		@Override
		public boolean isEmpty() {
			return list.isEmpty();
		}

		@Override
		public Iterator<Graphic> iterator() {
			return list.iterator();
		}

		@Override
		public boolean remove(Object o) {
			if (o instanceof Graphic) {
				Graphic g = (Graphic) o;
				if (g instanceof Refreshable<?>) {
					((Refreshable<?>) g).bind(null);
				}
				if (canvas().remove(g) && list.remove(g)) {
					return true;
				}
			}
			return false;
		}

		@Override
		public boolean removeAll(Collection<?> c) {
			boolean dirty = false;

			for (Object o : c) {
				if (o instanceof Layer<?>) {
					Graphic g = (Graphic) o;
					if (g instanceof Refreshable<?>) {
						((Refreshable<?>) g).bind(null);
					}
					dirty = dirty || canvas().remove(g);
				}
			}

			dirty = dirty || list.removeAll(c);

			return dirty;
		}

		@Override
		public boolean retainAll(Collection<?> c) {
			boolean dirty = false;

			for (Graphic g : list) {
				if (!c.contains(g)) {
					if (g instanceof Refreshable<?>) {
						((Refreshable<?>) g).bind(null);
					}
					dirty = dirty || canvas().remove(g);
				}
			}

			dirty = dirty || list.retainAll(c);

			return dirty;
		}

		@Override
		public int size() {
			return list.size();
		}

		@Override
		public Object[] toArray() {
			return list.toArray();
		}

		@Override
		public <T> T[] toArray(T[] a) {
			return list.toArray(a);
		}
	};

	private Collection<Layer<? extends S>> layersRef = new Collection<Layer<? extends S>>() {

		private List<Layer<? extends S>> list = new ArrayList<Layer<? extends S>>();

		/**
		 * Null layers or layers of different projection from this viewer will
		 * be refused to add. The display order of appended layers is determined
		 * by z-Index style of each layer.
		 */
		@Override
		public boolean add(Layer<? extends S> layer) {
			if (layer != null && layer.service() != null
					&& layer.service().isBindableWith(viewport())
					&& list.add(layer)) {
				layer.service().tryBind(viewport());
				if (!layer.service().isEnabled()) {
					layer.style().setVisible(false);
				}
				content().add(layer);
				layer.style().setVisible(true);
				layer.refresh();
				return true;
			}
			return false;
		}

		@Override
		public boolean addAll(Collection<? extends Layer<? extends S>> c) {
			boolean dirty = false;
			for (Layer<? extends S> l : c) {
				dirty = dirty || add(l);
			}
			return dirty;
		}

		@Override
		public void clear() {
			for (Layer<? extends S> l : list) {
				l.service().tryBind(null);
				content().remove(l);
			}
			list.clear();
		}

		@Override
		public boolean contains(Object o) {
			return list.contains(o);
		}

		@Override
		public boolean containsAll(Collection<?> c) {
			return list.containsAll(c);
		}

		@Override
		public boolean isEmpty() {
			return list.isEmpty();
		}

		@Override
		public Iterator<Layer<? extends S>> iterator() {
			return list.iterator();
		}

		@Override
		public boolean remove(Object o) {
			if (o instanceof Layer<?>) {
				Layer<?> l = (Layer<?>) o;
				l.service().tryBind(null);
				if (content().remove(l) && list.remove(l)) {
					return true;
				}
			}
			return false;
		}

		@Override
		public boolean removeAll(Collection<?> c) {
			boolean dirty = false;

			for (Object o : c) {
				if (o instanceof Layer<?>) {
					Layer<?> l = (Layer<?>) o;
					l.service().tryBind(null);
					dirty = dirty || content().remove(l);
				}
			}

			dirty = dirty || list.removeAll(c);

			return dirty;
		}

		@Override
		public boolean retainAll(Collection<?> c) {
			boolean dirty = false;

			for (Layer<? extends S> l : list) {
				if (!c.contains(l)) {
					l.service().tryBind(null);
					dirty = dirty || content().remove(l);
				}
			}

			dirty = dirty || list.retainAll(c);

			return dirty;
		}

		@Override
		public int size() {
			return list.size();
		}

		@Override
		public Object[] toArray() {
			return list.toArray();
		}

		@Override
		public <T> T[] toArray(T[] a) {
			return list.toArray(a);
		}

	};

	private Collection<Overlay> overlaysRef = new Collection<Overlay>() {

		private List<Overlay> list = new ArrayList<Overlay>();

		@Override
		public boolean add(Overlay overlay) {
			if (overlay == null) {
				return false;
			}

			if (!(overlay instanceof Refreshable<?>)) {
				// TODO check bounds
				floatingContent().add(overlay);
				return true;
			}

			Refreshable<?> refreshable = (Refreshable<?>) overlay;
			if (refreshable.service() != null
					&& refreshable.service().isBindableWith(viewport())
					&& list.add(overlay)) {
				refreshable.service().tryBind(viewport());
				if (!refreshable.service().isEnabled()) {
					overlay.style().setVisible(false);
				}
				floatingContent().add(overlay);
				DOM.appendChild(floatingContent().getElement(), overlay
						.asWidget().getElement());
				overlay.style().setVisible(true);
				refreshable.refresh();
				return true;
			}

			return false;
		}

		@Override
		public boolean addAll(Collection<? extends Overlay> c) {
			boolean dirty = false;
			for (Overlay overlay : c) {
				dirty = dirty || add(overlay);
			}
			return dirty;
		}

		@Override
		public void clear() {
			for (Overlay overlay : list) {
				if (overlay instanceof Refreshable<?>) {
					((Refreshable<?>) overlay).bind(null);
				}
				canvasRef.remove(overlay);
			}
			list.clear();
		}

		@Override
		public boolean contains(Object o) {
			return list.contains(o);
		}

		@Override
		public boolean containsAll(Collection<?> c) {
			return list.containsAll(c);
		}

		@Override
		public boolean isEmpty() {
			return list.isEmpty();
		}

		@Override
		public Iterator<Overlay> iterator() {
			return list.iterator();
		}

		@Override
		public boolean remove(Object o) {
			if (o instanceof Overlay) {
				Overlay overlay = (Overlay) o;
				if (overlay instanceof Refreshable<?>) {
					((Refreshable<?>) overlay).bind(null);
				}
				if (floatingContent().remove(overlay) && list.remove(overlay)) {
					return true;
				}
			}
			return false;
		}

		@Override
		public boolean removeAll(Collection<?> c) {
			boolean dirty = false;

			for (Object o : c) {
				if (o instanceof Layer<?>) {
					Overlay overlay = (Overlay) o;
					if (overlay instanceof Refreshable<?>) {
						((Refreshable<?>) overlay).bind(null);
					}
					dirty = dirty || floatingContent().remove(overlay);
				}
			}

			dirty = dirty || list.removeAll(c);

			return dirty;
		}

		@Override
		public boolean retainAll(Collection<?> c) {
			boolean dirty = false;

			for (Overlay overlay : list) {
				if (!c.contains(overlay)) {
					if (overlay instanceof Refreshable<?>) {
						((Refreshable<?>) overlay).bind(null);
					}
					dirty = dirty || floatingContent().remove(overlay);
				}
			}

			dirty = dirty || list.retainAll(c);

			return dirty;
		}

		@Override
		public int size() {
			return list.size();
		}

		@Override
		public Object[] toArray() {
			return list.toArray();
		}

		@Override
		public <T> T[] toArray(T[] a) {
			return list.toArray(a);
		}
	};

	private M viewportRef;

	public GFXPanel() {
		super(focusImpl.createFocusable());

		contentRef.getElement().getStyle().setPosition(Position.ABSOLUTE);
		contentRef.addStyleName("panelContent");
		add(contentRef);

		canvasRef.getElement().getStyle().setPosition(Position.ABSOLUTE);
		canvasRef.addStyleName("panelCanvas");
		add(canvasRef);

		floatingContentRef.getElement().getStyle()
				.setPosition(Position.ABSOLUTE);
		floatingContentRef.addStyleName("panelFloatingContent");
		add(floatingContentRef);

		addStyleName("gfx-Panel");
	}

	@Override
	public HandlerRegistration addBlurHandler(BlurHandler handler) {
		return addDomHandler(handler, BlurEvent.getType());
	}

	@Override
	public HandlerRegistration addClickHandler(ClickHandler handler) {
		return addDomHandler(handler, ClickEvent.getType());
	}

	@Override
	public HandlerRegistration addDoubleClickHandler(DoubleClickHandler handler) {
		return addDomHandler(handler, DoubleClickEvent.getType());
	}

	@Override
	public HandlerRegistration addFocusHandler(FocusHandler handler) {
		return addDomHandler(handler, FocusEvent.getType());
	}

	@Override
	public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
		return addDomHandler(handler, KeyDownEvent.getType());
	}

	@Override
	public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
		return addDomHandler(handler, KeyPressEvent.getType());
	}

	@Override
	public HandlerRegistration addKeyUpHandler(KeyUpHandler handler) {
		return addDomHandler(handler, KeyUpEvent.getType());
	}

	@Override
	public HandlerRegistration addMouseDownHandler(MouseDownHandler handler) {
		return addDomHandler(handler, MouseDownEvent.getType());
	}

	@Override
	public HandlerRegistration addMouseMoveHandler(MouseMoveHandler handler) {
		return addDomHandler(handler, MouseMoveEvent.getType());
	}

	@Override
	public HandlerRegistration addMouseOutHandler(MouseOutHandler handler) {
		return addDomHandler(handler, MouseOutEvent.getType());
	}

	@Override
	public HandlerRegistration addMouseOverHandler(MouseOverHandler handler) {
		return addDomHandler(handler, MouseOverEvent.getType());
	}

	@Override
	public HandlerRegistration addMouseUpHandler(MouseUpHandler handler) {
		return addDomHandler(handler, MouseUpEvent.getType());
	}

	@Override
	public HandlerRegistration addMouseWheelHandler(MouseWheelHandler handler) {
		return addDomHandler(handler, MouseWheelEvent.getType());
	}

	public void bind(final M viewport) {
		viewportRef = viewport;
	}

	public GraphicPanel canvas() {
		return canvasRef;
	}

	public AbsolutePanel content() {
		return contentRef;
	}

	public AbsolutePanel floatingContent() {
		return floatingContentRef;
	}

	@Override
	public int getTabIndex() {
		return focusImpl.getTabIndex(getElement());
	}

	public Collection<Graphic> graphics() {
		return graphicsRef;
	}

	public Collection<Layer<? extends S>> layers() {
		return layersRef;
	}

	public Collection<Overlay> overlays() {
		return overlaysRef;
	}

	public void pan(int panWidth, int panHeight) {
		refresh();
	}

	/**
	 * Scale of this viewer will be adjusted first for better adaption to
	 * layers. Visible layers that of same scale as the query will be rendered
	 * in the call.
	 */
	public void refresh() {
		for (Layer<? extends S> layer : layers()) {
			if (!layer.service().isEnabled()) {
				layer.style().setVisible(false);
				continue;
			}
			layer.style().setVisible(true);
			layer.refresh();
		}
		for (Overlay overlay : overlays()) {
			// TODO check bounds
			if (overlay instanceof Refreshable<?>) {
				Refreshable<?> refreshable = (Refreshable<?>) overlay;
				if (!refreshable.service().isEnabled()) {
					overlay.style().setVisible(false);
					continue;
				}
				overlay.style().setVisible(true);
				refreshable.refresh();
			}
		}
	}

	@Override
	public void setAccessKey(char key) {
		focusImpl.setAccessKey(getElement(), key);
	}

	@Override
	public void setFocus(boolean focused) {
		if (focused) {
			focusImpl.focus(getElement());
		} else {
			focusImpl.blur(getElement());
		}
	}

	/**
	 * @param height
	 *            {@link #getOffsetHeight()}
	 */
	@Override
	public void setHeight(String height) {
		super.setHeight(height);
		contentRef.setHeight(height);
		canvasRef.setHeight(height);
		floatingContentRef.setHeight(height);
	}

	@Override
	public void setTabIndex(int index) {
		focusImpl.setTabIndex(getElement(), index);
	}

	/**
	 * @param width
	 *            {@link #getOffsetWidth()}
	 */
	@Override
	public void setWidth(String width) {
		super.setWidth(width);
		contentRef.setWidth(width);
		canvasRef.setWidth(width);
		floatingContentRef.setWidth(width);
	}

	public M viewport() {
		return viewportRef;
	}

}
